﻿using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace Black.SurfaceToolkit.Controls
{
    /// <summary>
    /// Représente un panel dont les éléments suivent le chemin spécifié.
    /// </summary>
    /// <remarks>
    /// <para>Ce contrôle dispose ses éléments enfants suivant le chemin <see cref="P:Black.SurfaceToolkit.Controls.PathPanel.Path" />.</para>
    /// <para><see cref="P:Black.SurfaceToolkit.Controls.PathPanel.Resolution" /> détermine l'espacement entre les éléments enfants.</para>
    /// <para><see cref="P:Black.SurfaceToolkit.Controls.PathPanel.HorizontalLineAlignment" /> et <see cref="P:Black.SurfaceToolkit.Controls.PathPanel.VerticalLineAlignment" /> déterminent la position des éléments enfants par rapport au chemin <see cref="P:Black.SurfaceToolkit.Controls.PathPanel.Path" />.</para>
    /// <para><div class="caption" style="font-weight : bold">PathPanel</div></para>
    /// <para><img src="../Images/PathPanel.Normal.png" /></para>
    /// </remarks>
    [Browsable(true), Description("Dispose les éléments sur un chemin")]
    public class PathPanel : Panel
    {
        #region Propriétés de dépendance

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="P:Black.SurfaceToolkit.Controls.PathPanel.HorizontalLineAlignment" />.
        /// </summary>
        public static readonly DependencyProperty HorizontalLineAlignmentProperty = DependencyProperty.Register("HorizontalLineAlignment", typeof(HorizontalAlignment), typeof(PathPanel), new FrameworkPropertyMetadata(HorizontalAlignment.Center, FrameworkPropertyMetadataOptions.AffectsMeasure), new ValidateValueCallback(PathPanel.IsValidHorizontalLineAlignment));
        /// <summary>
        /// Identifie la propriété de dépendance <see cref="P:Black.SurfaceToolkit.Controls.PathPanel.Path" />.
        /// </summary>
        public static readonly DependencyProperty PathProperty = DependencyProperty.Register("Path", typeof(PathGeometry), typeof(PathPanel), new FrameworkPropertyMetadata(null));
        /// <summary>
        /// Identifie la propriété de dépendance <see cref="P:Black.SurfaceToolkit.Controls.PathPanel.Position" />.
        /// </summary>
        public static readonly DependencyProperty PositionProperty = DependencyProperty.RegisterAttached("Position", typeof(double), typeof(PathPanel), new FrameworkPropertyMetadata(double.NaN, FrameworkPropertyMetadataOptions.AffectsParentMeasure, null, new CoerceValueCallback(PathPanel.CoercePosition)), new ValidateValueCallback(PathPanel.IsNotInfinityDouble));
        /// <summary>
        /// Identifie la propriété de dépendance <see cref="P:Black.SurfaceToolkit.Controls.PathPanel.Resolution" />.
        /// </summary>
        public static readonly DependencyProperty ResolutionProperty = DependencyProperty.Register("Resolution", typeof(double), typeof(PathPanel), new FrameworkPropertyMetadata(double.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure), new ValidateValueCallback(PathPanel.IsNotInfinityDouble));
        /// <summary>
        /// Identifie la propriété de dépendance <see cref="P:Black.SurfaceToolkit.Controls.PathPanel.VerticalLineAlignment" />.
        /// </summary>
        public static readonly DependencyProperty VerticalLineAlignmentProperty = DependencyProperty.Register("VerticalLineAlignment", typeof(VerticalAlignment), typeof(PathPanel), new FrameworkPropertyMetadata(VerticalAlignment.Center, FrameworkPropertyMetadataOptions.AffectsMeasure), new ValidateValueCallback(PathPanel.IsValidVerticalAlignment));

        #endregion

        #region Accesseurs

        /// <summary>
        /// Obtient ou définit l'alignement vertical vis-à-vis de la ligne.
        /// </summary>
        [Browsable(true), DefaultValue(HorizontalAlignment.Center), Description("Alignement vertical vis-à-vis de la ligne"), Category("PathPanel")]
        public HorizontalAlignment HorizontalLineAlignment
        {
            get
            {
                return (HorizontalAlignment)this.GetValue(PathPanel.HorizontalLineAlignmentProperty);
            }
            set
            {
                this.SetValue(PathPanel.HorizontalLineAlignmentProperty, value);
            }
        }

        /// <summary>
        /// Obtient ou définit le chemin.
        /// </summary>
        [Browsable(false)]
        public PathGeometry Path
        {
            get
            {
                return (PathGeometry)this.GetValue(PathPanel.PathProperty);
            }
            set
            {
                this.SetValue(PathPanel.PathProperty, value);
            }
        }

        /// <summary>
        /// Obtient ou définit la résolution du chemin.
        /// </summary>
        [Browsable(true), DefaultValue(double.NaN), Description("Résolution du chemin"), Category("PathPanel")]
        public double Resolution
        {
            get
            {
                return (double)this.GetValue(PathPanel.ResolutionProperty);
            }
            set
            {
                this.SetValue(PathPanel.ResolutionProperty, value);
            }
        }

        /// <summary>
        /// Obtient ou définit l'alignement vertical vis-à-vis de la ligne.
        /// </summary>
        [Browsable(true), DefaultValue(VerticalAlignment.Center), Description("Alignement vertical vis-à-vis de la ligne"), Category("PathPanel")]
        public VerticalAlignment VerticalLineAlignment
        {
            get
            {
                return (VerticalAlignment)this.GetValue(PathPanel.VerticalLineAlignmentProperty);
            }
            set
            {
                this.SetValue(PathPanel.VerticalLineAlignmentProperty, value);
            }
        }

        #endregion

        #region Constructeurs

        /// <summary>
        /// Initialise une nouvelle instance de <see cref="T:Black.SurfaceToolkit.Controls.PathPanel" />.
        /// </summary>
        public PathPanel()
            : base()
        {
        }

        #endregion

        #region Fonctions protégées

        /// <summary>
        /// Réarrange l'élément et ses enfants.
        /// </summary>
        /// <param name="p_oArrangeSize">Taille maximum autorisée.</param>
        /// <returns>Taille utilisée.</returns>
        protected override Size ArrangeOverride(Size p_oArrangeSize)
        {
            PathGeometry oPath = this.Path;

            if (oPath != null)
            {
                UIElementCollection oChildren = this.Children;
                int iCount = oChildren.Count;
                VerticalAlignment eVerticalLineAlignment = this.VerticalLineAlignment;
                HorizontalAlignment eHorizontalLineAlignment = this.HorizontalLineAlignment;

                // Place les enfants

                for (int i = 0; i < iCount; i++)
                {
                    UIElement oChild = oChildren[i];

                    double dPosition = this.GetChildFractionalPosition(oChild, i);

                    Point oPoint, oTangent;
                    oPath.GetPointAtFractionLength(dPosition, out oPoint, out oTangent);

                    // Place l'origine du contrôle par rapport au chemin

                    switch (eVerticalLineAlignment)
                    {
                        case VerticalAlignment.Bottom:
                            oPoint.Y -= oChild.DesiredSize.Height;
                            break;
                        case VerticalAlignment.Center:
                            oPoint.Y -= oChild.DesiredSize.Height / 2.0;
                            break;
                    }

                    switch (eHorizontalLineAlignment)
                    {
                        case HorizontalAlignment.Right:
                            oPoint.X -= oChild.DesiredSize.Width;
                            break;
                        case HorizontalAlignment.Center:
                            oPoint.X -= oChild.DesiredSize.Width / 2.0;
                            break;
                    }

                    // Place le contrôle

                    Size oChildSize = new Size()
                    {
                        Width = Math.Max(0.0, Math.Min(oChild.DesiredSize.Width, p_oArrangeSize.Width - oPoint.X)),
                        Height = Math.Max(0.0, Math.Min(oChild.DesiredSize.Height, p_oArrangeSize.Height - oPoint.Y))
                    };

                    Rect oChildRectangle = new Rect(oPoint, oChildSize);
                    oChild.Arrange(oChildRectangle);
                }
            }

            return (p_oArrangeSize);
        }

        /// <summary>
        /// Mesure l'élément et ses enfants en prévision de leur réorganisation pendant la passe <see cref="M:Black.SurfaceToolkit.Controls.PathPanel.ArrangeOverride(System.Windows.Size)" />.
        /// </summary>
        /// <param name="p_oConstraint">Taille limite supérieure à ne pas dépasser.</param>
        /// <returns>Taille désirée de l'élément.</returns>
        protected override Size MeasureOverride(Size p_oConstraint)
        {
            Size oResult = new Size(0.0, 0.0);

            PathGeometry oPath = this.Path;

            if (oPath != null)
            {
                UIElementCollection oChildren = this.Children;
                int iCount = oChildren.Count;
                VerticalAlignment eVerticalLineAlignment = this.VerticalLineAlignment;
                HorizontalAlignment eHorizontalLineAlignment = this.HorizontalLineAlignment;

                // Trouve le maximum

                for (int i = 0; i < iCount; i++)
                {
                    UIElement oChild = oChildren[i];

                    double dPosition = this.GetChildFractionalPosition(oChild, i);

                    Point oPoint, oTangent;
                    oPath.GetPointAtFractionLength(dPosition, out oPoint, out oTangent);

                    // Calcule la taille maximum du contrôle par rapport au chemin

                    Size oChildSize = new Size();
                    switch (eVerticalLineAlignment)
                    {
                        case VerticalAlignment.Bottom:
                            oChildSize.Height = oPoint.Y;
                            break;
                        case VerticalAlignment.Center:
                            oChildSize.Height = Math.Min(oPoint.Y, p_oConstraint.Height - oPoint.Y) * 2;
                            break;
                        case VerticalAlignment.Top:
                            oChildSize.Height = p_oConstraint.Height - oPoint.Y;
                            break;
                    }

                    switch (eHorizontalLineAlignment)
                    {
                        case HorizontalAlignment.Right:
                            oChildSize.Width = oPoint.X;
                            break;
                        case HorizontalAlignment.Center:
                            oChildSize.Width = Math.Min(oPoint.X, p_oConstraint.Width - oPoint.X) * 2;
                            break;
                        case HorizontalAlignment.Left:
                            oChildSize.Width = p_oConstraint.Width - oPoint.X;
                            break;
                    }

                    oChild.Measure(oChildSize);

                    // Calcul la taille dont ce contrôle à besoin pour être affiché

                    Size oNewSize = new Size(0.0, 0.0);
                    switch (eVerticalLineAlignment)
                    {
                        case VerticalAlignment.Bottom:
                            oNewSize.Height = oPoint.Y;
                            break;
                        case VerticalAlignment.Center:
                            oNewSize.Height = oPoint.Y + oChild.DesiredSize.Height / 2.0;
                            break;
                        case VerticalAlignment.Top:
                            oNewSize.Height = oPoint.Y + oChild.DesiredSize.Height;
                            break;
                    }
                    switch (eHorizontalLineAlignment)
                    {
                        case HorizontalAlignment.Right:
                            oNewSize.Width = oPoint.X;
                            break;
                        case HorizontalAlignment.Center:
                            oNewSize.Width = oPoint.X + oChild.DesiredSize.Width / 2.0;
                            break;
                        case HorizontalAlignment.Left:
                            oNewSize.Width = oPoint.X + oChild.DesiredSize.Width;
                            break;
                    }

                    // Garde la taille maximum

                    oResult.Width = Math.Max(oResult.Width, oNewSize.Width);
                    oResult.Height = Math.Max(oResult.Height, oNewSize.Height);
                }
            }

            // Garantit que l'on ne sort pas de la zone de contrainte

            oResult.Width = Math.Min(oResult.Width, p_oConstraint.Width);
            oResult.Height = Math.Min(oResult.Height, p_oConstraint.Height);

            return (oResult);
        }

        #endregion

        #region Fonctions privées

        /// <summary>
        /// Obtient la position fractionnaire de l'élément spécifié.
        /// </summary>
        /// <param name="p_oChild">Elément dont on veut la position fractionnaire.</param>
        /// <param name="p_iIndex">Index de l'élément.</param>
        /// <returns>Position fractionnaire de <paramref name="p_oChild" /> d'index <paramref name="p_iIndex" />.</returns>
        private double GetChildFractionalPosition(UIElement p_oChild, int p_iIndex)
        {
            double dPosition = PathPanel.GetPosition(p_oChild);

            // Si la position n'est pas définit : distribution selon l'index et la résolution.

            if (double.IsNaN(dPosition))
            {
                double dResolution = this.Resolution;

                if (double.IsNaN(dResolution))
                {
                    double iChildrenCount = this.Children.Count;

                    if (iChildrenCount == 1)
                        dResolution = 0;
                    else
                        dResolution = 1.0 / (double)(iChildrenCount - 1);
                }

                dPosition = (double)p_iIndex * dResolution;
            }

            return (Math.Max(0.0, Math.Min(1.0, dPosition)));
        }

        #endregion

        #region Fonctions statiques publiques

        /// <summary>
        /// Obtient la position de l'élément spécifié.
        /// </summary>
        /// <param name="p_oElement">Elément dont on veut la position.</param>
        /// <returns>Position de <paramref name="p_oElement" />.</returns>
        /// <exception cref="T:System.ArgumentNullException"><paramref name="p_oElement" /> est <see langword="null" />.</exception>
        [AttachedPropertyBrowsableForChildren]
        public static double GetPosition(DependencyObject p_oElement)
        {
            if (p_oElement == null)
                throw new ArgumentNullException("p_oElement");

            return (double)p_oElement.GetValue(PathPanel.PositionProperty);
        }

        /// <summary>
        /// Définit la position d'un élément.
        /// </summary>
        /// <param name="p_oElement">Elément dont on veut définir la position.</param>
        /// <param name="value">Position de <paramref name="p_oElement" />.</param>
        /// <exception cref="T:System.ArgumentNullException"><paramref name="p_oElement" /> est <see langword="null" />.</exception>
        [AttachedPropertyBrowsableForChildren]
        public static void SetPosition(DependencyObject p_oElement, double value)
        {
            if (p_oElement == null)
                throw new ArgumentNullException("p_oElement");

            p_oElement.SetValue(PathPanel.PositionProperty, value);
        }

        #endregion

        #region Fonctions statiques privées

        /// <summary>
        /// Appelée lorsque la propriété <see cref="P:Black.SurfaceToolkit.Controls.PathPanel.Position" /> est réévaluée.
        /// </summary>
        /// <param name="p_oSender">Source de l'événement.</param>
        /// <param name="p_oValue">Valeur à adapter.</param>
        /// <returns><paramref name="p_oValue" /> adapté.</returns>
        private static object CoercePosition(DependencyObject p_oSender, object p_oValue)
        {
            double dPosition = (double)p_oValue;

            return (Math.Max(0.0, Math.Min(1.0, dPosition)));
        }

        /// <summary>
        /// Valide un alignement horizontal sur la ligne (non <see cref="F:System.Windows.HorizontalAlignment.Stretch" />).
        /// </summary>
        /// <param name="p_oValue">Alignement horizontal sur la ligne à valider.</param>
        /// <returns>Valeur indiquant si <paramref name="p_oValue" /> est valide (non <see cref="F:System.Windows.HorizontalAlignment.Stretch" />).</returns>
        private static bool IsValidHorizontalLineAlignment(object p_oValue)
        {
            HorizontalAlignment eHorizontalAlignment = (HorizontalAlignment)p_oValue;

            return (eHorizontalAlignment != HorizontalAlignment.Stretch);
        }

        /// <summary>
        /// Valide un nombre non infini.
        /// </summary>
        /// <param name="p_oValue">Nombre à valider.</param>
        /// <returns>Valeur indiquant si <paramref name="p_oValue" /> est non infini.</returns>
        private static bool IsNotInfinityDouble(object p_oValue)
        {
            double dValue = (double)p_oValue;

            return (!double.IsInfinity(dValue));
        }

        /// <summary>
        /// Valide un alignement vertical sur la ligne (non <see cref="F:System.Windows.VerticalAlignment.Stretch" />).
        /// </summary>
        /// <param name="p_oValue">Alignement vertical sur la ligne à valider.</param>
        /// <returns>Valeur indiquant si <paramref name="p_oValue" /> est valide (non <see cref="F:System.Windows.VerticalAlignment.Stretch" />).</returns>
        private static bool IsValidVerticalAlignment(object p_oValue)
        {
            VerticalAlignment eVerticalAlignment = (VerticalAlignment)p_oValue;

            return (eVerticalAlignment != VerticalAlignment.Stretch);
        }

        #endregion
    }
}
